//定时器实现

import java.util.PriorityQueue;

// 创建一个类, 用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
    // 任务啥时候执行. 毫秒级的时间戳
    private Long time;

    //具体任务是什么
    Runnable runnable;

    public MyTimerTask(Long delay, Runnable runnable) {
        // delay 是一个相对的时间差. 形如 3000 这样的数值.
        // 构造 time 要根据当前系统时间和 delay 进行构造.
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    public Long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
//        return (int)(o.time - this.time);
    }

    //随便写一个类，它的对象都能放到优先级队列中嘛？有没有啥特别的要求？
    //要求放到优先级队列中的元素，是"可比较的"
    // 通过Comparable或者Comparator定义任务之间的比较规则
    @Override
    public String toString() {
        return "MyTimerTask{" +
                "time=" + time +
                ", runnable=" + runnable +
                '}';
    }
}

// 定时器类的本体
class MyTimer {
    // 使用优先级队列, 来保存上述的 N 个任务
    //这个集合类，不是线程安全的
    //既会在主线程中使用
    //又会在扫描线程中使用
    //针对queue的操作，进行加锁
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    // 用来加锁的对象
    private Object locker = new Object();

    // 定时器的核心方法, 就是把要执行的任务添加到队列中.
    public void schedule(Runnable runnable, Long delay) {
        synchronized (locker) {
            MyTimerTask myTimerTask = new MyTimerTask(delay, runnable);
            queue.offer(myTimerTask);
            // 每次来新的任务, 都唤醒一下之前的扫描线程. 好让扫描线程根据最新的任务情况, 重新规划等待时间
            locker.notify();
        }
    }

    // MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer() {
        // 扫描线程
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 注意, 当前如果队列为空, 此时就不应该去取这里的元素
                            // 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快
                            // 也会陷入一个高频占用 cpu 的状态(忙等)
                            locker.wait();
                        }
                        MyTimerTask myTimerTask = queue.peek();
                        Long curTime = System.currentTimeMillis();
                        if (curTime >= myTimerTask.getTime()) {
                            // 假设当前时间是 14:01, 任务时间是 14:00, 此时就意味着应该要执行这个任务了.
                            // 需要执行任务.
                            queue.poll();
                            myTimerTask.getRunnable().run();
                        } else {
                            //扫描线程中，直接使用sleep进行休眠，是否合适呢?
                            //不合适
                            //sleep 进入阻塞后，不会释放锁,会影响到其他线程执行schedule
                            //sleep在休眠的过程中，不方便提前中断.(虽然可以使用interrupt来中断，但是interrupt意味着线程应该要结束了)
                            //假设当前最靠前的任务，是14:00执行
                            //当前时刻是 13:00 (sleep一小时)
                            //此时如果我们新增一个新的任务，新的任务是13:30要执行的
                            //新的任务，成了最早要执行的任务了
                            //每次来新的任务，都能把之前的休眠的状态，给唤醒，并且根据当前的最新的任务情况，重新进行判定
                            //此时，扫描线程就可以按照30分钟，来进行阻塞等待了

                            // 让当前扫描线程休眠一下, 按照时间差来进行休眠
                            locker.wait(myTimerTask.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
    }
}

public class Demo23 {
    public static void main(String[] args) {

        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        }, 5000L);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        }, 2000L);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1");
            }
        }, 1000L);
        System.out.println("程序开始运行");
    }
}
